<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" name="viewport" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        body {
            user-select: none;
        }

        .holder2048 {
            width: 280px;
            height: 380px;
            position: relative;
            margin: 0 auto;
        }

        .holder2048>.container {
            width: 280px;
            height: 280px;
            position: relative;
            margin: 0 auto;
            border-style: none;
            border: 3px solid #BBADA0;
            background-color: #CCC0B3;
        }

        .holder2048>.container>.mask {
            width: 70px;
            height: 70px;
            position: absolute;
            box-sizing: border-box;
            -moz-box-sizing: border-box;
            -webkit-box-sizing: border-box;
            border: 3px solid #BBADA0;
        }

        .holder2048>.container>.box {
            width: 66px;
            height: 66px;
            background-color: Black;
            position: absolute;
            color: #776E65;
            font-size: x-large;
            font-weight: bolder;
            font-family: Arial;
            text-align: center;
            line-height: 70px;
        }

        .holder2048>.container>.box[value='2'] {
            background-color: #FFF8D7
        }

        .holder2048>.container>.box[value='4'] {
            background-color: #FFED97
        }

        .holder2048>.container>.box[value='8'] {
            background-color: #FFBB77
        }

        .holder2048>.container>.box[value='16'] {
            background-color: #FF9224
        }

        .holder2048>.container>.box[value='32'] {
            background-color: #FF5809
        }

        .holder2048>.container>.box[value='64'] {
            background-color: #EA0000
        }

        .holder2048>.container>.box[value='128'] {
            background-color: #FFFF37
        }

        .holder2048>.container>.box[value='256'] {
            background-color: #F9F900
        }

        .holder2048>.container>.box[value='512'] {
            background-color: #E1E100
        }

        .holder2048>.container>.box[value='1024'] {
            background-color: #C4C400
        }

        .holder2048>.container>.box[value='2048'] {
            background-color: #9AFF02
        }

        .holder2048>.container>.box[value='4096'] {
            background-color: #00FFFF
        }

        .holder2048>.container>.box[value='8192'] {
            background-color: #FF00FF
        }
        /* == */
        h1 {
            text-align: center;
            margin-bottom: 30px;
            vertical-align: middle;
        }
        button {
            background-color: #fff;
            border: 1px solid #ccc;
            height: 30px;
            line-height: 30px;
            vertical-align: middle;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <h1>
        通过四个方向键移动
        <button class="restart">重新开始</button>
    </h1>
    <div class="container text-center" id="2048">
    </div>
    <script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
    <script>
        (function ($) {
            /**
             * User options
             */
            var defaults = {
                delay: 200 //Game speed
            };

            $.fn.init2048 = function (_options) {
                var _this = this,
                    options = $.extend(defaults, _options),

                    dir = {
                        up: 'up',
                        right: 'right',
                        down: 'down',
                        left: 'left'
                    },

                    holder = {}, //Game outer holder
                    content = {}, //Game inner container

                    matrix = [], //For the logic behind
                    boxes = [], //Boxes storage

                    isCheating = 0,
                    isGameOver = false;

                resetGame();
                bind();

                /**
                 * Restart the game by recreate all DOM elements.
                 */
                function resetGame() {
                    //Reset the props
                    boxes = [];
                    matrix = [];
                    isCheating = 0;
                    isGameOver = false;
                    //Recreate DOM elements
                    holder = $('<div>').addClass('holder2048');
                    content = $('<div>').addClass('container').appendTo(holder);
                    for (var i = 0; i < 4; i++) {
                        for (var j = 0; j < 4; j++) {
                            //Reset matrix
                            matrix[i * 4 + j] = {
                                top: i * 70,
                                left: j * 70,
                                taken: false,
                                combined: false,
                                value: 0
                            };
                            //This is for the borders of each cell.
                            $('<div>').addClass('mask').css({
                                left: j * 70 + "px",
                                top: i * 70 + "px"
                            }).appendTo(content);
                        }
                    }
                    //Create the first box on board
                    createBox();
                    //Insert game holder and anything to whoever calls this function
                    _this.html(holder);
                }

                /**
                 * Just for fun.
                 * Reset the game and place a 4096 box on the board.
                 */
                function cheat() {
                    resetGame();
                    createBox(4096);
                }

                /**
                 * Create a box and add to game
                 * Takes 1 or 0 param.
                 *
                 * @param value
                 */
                function createBox(value) {
                    //Check if there are spaces for a new box or not
                    var emptyMatrix = 0;
                    for (var i = 0; i < matrix.length; i++) {
                        if (!matrix[i].taken) {
                            emptyMatrix++;
                        }
                    }
                    if (emptyMatrix === 0) {
                        return;
                    }
                    //Chose an actual index (means not taken) randomly for the new box
                    var random = Math.floor(Math.random() * emptyMatrix + 1);
                    var chosenIndex = 0;
                    for (var j = 0; chosenIndex < matrix.length; chosenIndex++) {
                        while (matrix[chosenIndex].taken) {
                            chosenIndex++;
                        }
                        if (++j === random) {
                            matrix[chosenIndex].taken = true;
                            break;
                        }
                    }
                    //Do the create job
                    value = value ? value : (Math.floor(Math.random() * 4 + 1) === 4 ? 4 : 2); //Use the value parse in or (1/4 -> 4 or  3/4 -> 2)
                    var newBox = $('<div>').addClass('box').attr({
                        position: chosenIndex,
                        value: value
                    }).css({
                        marginTop: matrix[chosenIndex].top + 2,
                        marginLeft: matrix[chosenIndex].left + 2,
                        opacity: 0
                    }).text(value).appendTo(content).animate({
                        opacity: 1
                    }, options.delay * 2);
                    //Finally push it to the boxes array
                    boxes.push(newBox);
                }

                /**
                 * Combine 2 boxes into 1
                 * @param source
                 * @param target
                 * @param value
                 */
                function combineBox(source, target, value) {
                    var _value = parseInt(value) * 2;
                    boxes[target].attr('value', _value).html(_value).css({
                        zIndex: 99
                    }).animate({
                        width: '+=20',
                        height: '+=20',
                        marginTop: '-=10',
                        marginLeft: '-=10'
                    }, options.delay / 2, function () {
                        $(this).animate({
                            width: '-=20',
                            height: '-=20',
                            marginTop: '+=10',
                            marginLeft: '+=10'
                        }, options.delay / 2, function () {
                            $(this).css({
                                zIndex: 1
                            })
                        })
                    });
                    boxes[source].remove();
                    boxes.splice(source, 1);
                }

                /**
                 * Check if game over
                 * @returns {boolean}
                 */
                function gameOver() {
                    if (boxes.length != 16) {
                        return false;
                    }
                    var i, a, b;
                    for (i = 0; i < 16; i++) {
                        for (a = 0; a < 16; a++) {
                            if (boxes[a].attr('position') == i)
                                break;
                        }
                        if (i % 4 != 3) {
                            for (b = 0; b < 16; b++) {
                                if (boxes[b].attr('position') == i + 1)
                                    break;
                            }
                            if (boxes[a].attr('value') == boxes[b].attr('value'))
                                return false;
                        }
                        if (i < 12) {
                            for (b = 0; b < 16; b++) {
                                if (boxes[b].attr('position') == i + 4)
                                    break;
                            }
                            if (boxes[a].attr('value') == boxes[b].attr('value'))
                                return false;
                        }
                    }
                    return true;
                }

                /**
                 * Game run
                 * @param dir
                 */
                function gameRun(dir) {
                    if (isGameOver) {
                        return;
                    }
                    if (run(dir)) {
                        createBox();
                    }
                    if (gameOver()) {
                        isGameOver = true;
                        alert("Game Over");
                    }
                }

                /**
                 * Bind keyboard and screen touch events to game
                 */
                function bind() {
                    $(window).keydown(function (event) {
                        var keyCodeArray = [
                            { key: dir.left,  code: 37 },
                            { key: dir.up,    code: 38 },
                            { key: dir.right, code: 39 },
                            { key: dir.down,  code: 40 }
                        ]

                        var index = keyCodeArray.findIndex(function (item, index) {
                            return event.which === item.code
                        })
                        if (index === -1) { return; }
                        event.preventDefault();
                        gameRun(keyCodeArray[index]['key']);
                    });

                    var touchStartClientX, touchStartClientY;
                    document.addEventListener("touchstart", function (event) {
                        if (event.touches.length > 1)
                            return;
                        touchStartClientX = event.touches[0].clientX;
                        touchStartClientY = event.touches[0].clientY;
                    });
                    document.addEventListener("touchmove", function (event) {
                        event.preventDefault();
                    });
                    document.addEventListener("touchend", function (event) {
                        if (event.touches.length > 0)
                            return;
                        var dx = event.changedTouches[0].clientX - touchStartClientX;
                        var absDx = Math.abs(dx);
                        var dy = event.changedTouches[0].clientY - touchStartClientY;
                        var absDy = Math.abs(dy);
                        if (Math.max(absDx, absDy) > 10) {
                            if (absDx > absDy) {
                                if (dx > 0) {
                                    gameRun(dir.right);
                                } else {
                                    gameRun(dir.left);
                                }
                            } else {
                                if (dy > 0) {
                                    gameRun(dir.down);
                                } else {
                                    gameRun(dir.up);
                                }
                            }
                        }
                    });
                }

                /**
                 * [WARNING] This method is ugly enough for now. Waiting for refactor.
                 *
                 * Make a single game move.
                 * Takes 1 param.
                 *
                 * @param dir
                 * @returns {boolean}
                 */
                function run(dir) {
                    var isMoved = false; //This is to indicate that if the game actually moved after calling this function
                    var i, j, k, empty, _empty, position, value1, value2, temp; //Junks
                    //Reset the matrix attr 'combined' before moving
                    for (i = 0; i < 16; i++) {
                        matrix[i].combined = false;
                    }
                    if (dir == "left") {
                        isCheating = -1;
                        for (i = 0; i < 4; i++) {
                            empty = i * 4;
                            _empty = empty;
                            for (j = 0; j < 4; j++) {
                                position = i * 4 + j;
                                if (!matrix[position].taken) {
                                    continue;
                                }
                                if (matrix[position].taken && position === empty) {
                                    empty++;
                                    if (empty - 2 >= _empty) {
                                        for (k = 0; k < boxes.length; k++) {
                                            if (boxes[k].attr("position") == position) {
                                                break;
                                            }
                                        }
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty - 2) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty - 2].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty - 1].taken = false;
                                            matrix[empty - 2].combined = true;
                                            empty--;
                                            isMoved = true;
                                        }
                                    }
                                } else {
                                    for (k = 0; k < boxes.length; k++) {
                                        if (boxes[k].attr("position") == position) {
                                            break;
                                        }
                                    }
                                    boxes[k].animate({
                                        marginLeft: matrix[empty].left + 2,
                                        marginTop: matrix[empty].top + 2
                                    }, options.delay);
                                    boxes[k].attr('position', empty);
                                    matrix[empty].taken = true;
                                    matrix[position].taken = false;
                                    empty++;
                                    isMoved = true;
                                    if (empty - 2 >= _empty) {
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty - 2) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty - 2].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty - 1].taken = false;
                                            matrix[empty - 2].combined = true;
                                            empty--;
                                        }
                                    }
                                }
                            }
                        }
                    } else if (dir == "right") {
                        isCheating = -1;
                        for (i = 3; i > -1; i--) {
                            empty = i * 4 + 3;
                            _empty = empty;
                            for (j = 3; j > -1; j--) {
                                position = i * 4 + j;
                                if (!matrix[position].taken) {
                                    continue;
                                }
                                if (matrix[position].taken && position === empty) {
                                    empty--;
                                    if (empty + 2 <= _empty) {
                                        for (k = 0; k < boxes.length; k++) {
                                            if (boxes[k].attr("position") == position) {
                                                break;
                                            }
                                        }
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty + 2) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty + 2].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty + 1].taken = false;
                                            matrix[empty + 2].combined = true;
                                            empty++;
                                            isMoved = true;
                                        }
                                    }
                                } else {
                                    for (k = 0; k < boxes.length; k++) {
                                        if (boxes[k].attr("position") == position) {
                                            break;
                                        }
                                    }
                                    boxes[k].animate({
                                        marginLeft: matrix[empty].left + 2,
                                        marginTop: matrix[empty].top + 2
                                    }, options.delay);
                                    boxes[k].attr('position', empty);
                                    matrix[empty].taken = true;
                                    matrix[position].taken = false;
                                    empty--;
                                    isMoved = true;
                                    if (empty + 2 <= _empty) {
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty + 2) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty + 2].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty + 1].taken = false;
                                            matrix[empty + 2].combined = true;
                                            empty++;
                                        }
                                    }
                                }
                            }
                        }
                    } else if (dir == "up") {
                        isCheating = -1;
                        for (i = 0; i < 4; i++) {
                            empty = i;
                            _empty = empty;
                            for (j = 0; j < 4; j++) {
                                position = j * 4 + i;
                                if (!matrix[position].taken) {
                                    continue;
                                }
                                if (matrix[position].taken && position === empty) {
                                    empty += 4;
                                    if (empty - 8 >= _empty) {
                                        for (k = 0; k < boxes.length; k++) {
                                            if (boxes[k].attr("position") == position) {
                                                break;
                                            }
                                        }
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty - 8) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty - 8].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty - 4].taken = false;
                                            matrix[empty - 8].combined = true;
                                            empty -= 4;
                                            isMoved = true;
                                        }
                                    }
                                } else {
                                    for (k = 0; k < boxes.length; k++) {
                                        if (boxes[k].attr("position") == position) {
                                            break;
                                        }
                                    }
                                    boxes[k].animate({
                                        marginLeft: matrix[empty].left + 2,
                                        marginTop: matrix[empty].top + 2
                                    }, options.delay);
                                    boxes[k].attr('position', empty);
                                    matrix[empty].taken = true;
                                    matrix[position].taken = false;
                                    empty += 4;
                                    isMoved = true;
                                    if (empty - 8 >= _empty) {
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty - 8) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty - 8].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty - 4].taken = false;
                                            matrix[empty - 8].combined = true;
                                            empty -= 4;
                                        }
                                    }
                                }
                            }
                        }
                    } else if (dir == "down") {
                        if (isCheating != -1) {
                            isCheating++;
                            if (isCheating == 10) {
                                cheat();
                                return true;
                            }
                        }
                        for (i = 0; i < 4; i++) {
                            empty = i + 12;
                            _empty = empty;
                            for (j = 3; j > -1; j--) {
                                position = j * 4 + i;
                                if (!matrix[position].taken) {
                                    continue;
                                }
                                if (matrix[position].taken && position === empty) {
                                    empty -= 4;
                                    if (empty + 8 <= _empty) {
                                        for (k = 0; k < boxes.length; k++) {
                                            if (boxes[k].attr("position") == position) {
                                                break;
                                            }
                                        }
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty + 8) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty + 8].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty + 4].taken = false;
                                            matrix[empty + 8].combined = true;
                                            empty += 4;
                                            isMoved = true;
                                        }
                                    }
                                } else {
                                    for (k = 0; k < boxes.length; k++) {
                                        if (boxes[k].attr("position") == position) {
                                            break;
                                        }
                                    }
                                    boxes[k].animate({
                                        marginLeft: matrix[empty].left + 2,
                                        marginTop: matrix[empty].top + 2
                                    }, options.delay);
                                    boxes[k].attr('position', empty);
                                    matrix[empty].taken = true;
                                    matrix[position].taken = false;
                                    empty -= 4;
                                    isMoved = true;
                                    if (empty + 8 <= _empty) {
                                        value1 = boxes[k].attr('value');
                                        for (temp = 0; temp < boxes.length; temp++) {
                                            if (boxes[temp].attr("position") == empty + 8) {
                                                break;
                                            }
                                        }
                                        value2 = boxes[temp].attr('value');
                                        if (value1 == value2 && !matrix[empty + 8].combined) {
                                            combineBox(k, temp, value1);
                                            matrix[empty + 4].taken = false;
                                            matrix[empty + 8].combined = true;
                                            empty += 4;
                                        }
                                    }
                                }
                            }
                        }

                    }
                    return isMoved;
                }
            }
        })(jQuery);
    </script>
    <script>
        $(document).ready(function () {
            $("#2048").init2048();
            $('.restart').on('click', function () {
                $("#2048").init2048();
            })
        });
    </script>
</body>

</html>